Golang : Simple client-server HMAC authentication without SSL example
Problem:
You are building a distributed system that involves multiple processes communicating across a cluster of computers. You want to make sure only authenticated processes are allowed to connect to one another. Your requirement is just to authenticate a connection handshake, not encrypt the connection and thus you do not need SSL connection between the processes. How to implement a simple inter processes authentication?
Solution:
Use hmac
(Keyed-Hash Message Authentication Code) on both client and server to compute a hash known **only**
to both. The hash digest will be computed from a **secret key**
known only to client and server.
The basic idea is:
- The server will send the client a message of random bytes(string).
- The client will then compute a digest of the random bytes(string) received from the server with the common secret key.
- The client sends back the computed digest to the server.
- The server compares received digest from the client and decide whether or not to accept the connection request from client.
We will take the previous example of Golang client-server and add-on the connection handshake authentication process.
Let's enchance the server program first to include HMAC authentication mechanism
hmacserver.go
package main
import (
"crypto/hmac"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"net"
)
// both client and server MUST have the same secret key
// to authenticate
var secret = "GolangIsAwesome!"
func randStr(strSize int, randType string) string {
var dictionary string
if randType == "alphanum" {
dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "alpha" {
dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
}
if randType == "number" {
dictionary = "0123456789"
}
var bytes = make([]byte, strSize)
rand.Read(bytes)
for k, v := range bytes {
bytes[k] = dictionary[v%byte(len(dictionary))]
}
return string(bytes)
}
func serverSideAuthenticate(clientConn net.Conn, secretKey string) {
// request client authentication
// send client a random string as message
message := randStr(16, "alphanum")
_, err := clientConn.Write([]byte(message))
if err != nil {
clientConn.Close()
}
fmt.Println("Data send to client : ", message)
// prepare server side hmac digest
// with secret key and msg
hasher := hmac.New(md5.New, []byte(secretKey))
hasher.Write([]byte(message))
serverHMACdigest := hasher.Sum(nil)
fmt.Println("Server : ", base64.StdEncoding.EncodeToString(serverHMACdigest))
// receive hmacDigest from client
buffer := make([]byte, 4096)
n, err := clientConn.Read(buffer)
if err != nil || n == 0 {
clientConn.Close()
return
}
// don't over read n length
clientHMACdigest := buffer[:n]
fmt.Println("Client : ", base64.StdEncoding.EncodeToString(clientHMACdigest))
// compare if the server and client HMAC digests are the same or not
// the HANDSHAKING part!
fmt.Println("Connection authenticated : ", hmac.Equal(serverHMACdigest, clientHMACdigest))
// this is where you want to do stuff like disconnect client if the authentication failed
// or proceed
}
func handleConnection(c net.Conn) {
log.Printf("Client %v connected.", c.RemoteAddr())
serverSideAuthenticate(c, secret)
log.Printf("Connection from %v closed.", c.RemoteAddr())
}
func main() {
ln, err := net.Listen("tcp", ":6000")
if err != nil {
log.Fatal(err)
}
fmt.Println("Server up and listening on port 6000")
for {
conn, err := ln.Accept()
if err != nil {
log.Println(err)
continue
}
go handleConnection(conn)
}
}
and
hmacclient.go
package main
import (
"crypto/hmac"
"crypto/md5"
"encoding/base64"
"fmt"
"net"
)
// both client and server MUST have the same secret key
// to authenticate
var secret = "GolangIsAwesome!"
// change the secret to something else and the authentication will fail
//var secret = "GolangIsTerrible!"
func clientSideAuthenticate(serverConn net.Conn, secretKey string, message string) {
// prepare client side hmac digest
// with secret key and message received from server
// if the secret key in client and server is the same
// the digest should be the same.
hasher := hmac.New(md5.New, []byte(secretKey))
hasher.Write([]byte(message))
clientHMACdigest := hasher.Sum(nil)
fmt.Println("Digest send to server : ", base64.StdEncoding.EncodeToString(clientHMACdigest))
// send hmacDigest back to server to authenticate
n, err := serverConn.Write(clientHMACdigest)
if err != nil || n == 0 {
serverConn.Close()
return
}
}
func handleConnection(c net.Conn) {
buffer := make([]byte, 4096)
for {
n, err := c.Read(buffer)
if err != nil || n == 0 {
c.Close()
break
}
// don't over read n length
msg := string(buffer[:n])
fmt.Println("\nData received from server : ", msg)
clientSideAuthenticate(c, secret, msg)
}
fmt.Printf("Connection from %v closed. \n", c.RemoteAddr())
}
func main() {
hostName := "localhost" // change this to your server domain name
portNum := "6000"
for {
dialConn, err := net.Dial("tcp", hostName+":"+portNum)
if err != nil {
fmt.Println(err)
continue
}
fmt.Printf("\nConnection established between %s and localhost.\n", hostName)
fmt.Printf("Remote Address : %s \n", dialConn.RemoteAddr().String())
fmt.Printf("Local Address : %s \n", dialConn.LocalAddr().String())
go handleConnection(dialConn)
}
}
Run hmacserver
on one terminal and hmacclient
on another terminal/machine.
If the secret keys are the same in both client and server, you will see that the connection authenticated equals to TRUE
2016/07/03 18:27:19 Client [::1]:60825 connected.
Data send to client : iIFO2H2vHpSrwB6S
Server : G3lkRXxCYToGapXzdaoovQ==
Client : G3lkRXxCYToGapXzdaoovQ==
Connection authenticated :
true
2016/07/03 18:27:19 Connection from [::1]:60825 closed.
and if not
2016/07/03 17:56:19 Client [::1]:58660 connected.
Data send to client : Zqm6axXwzkYP0UP0
Server : y4E8I4MTjdZFgjna6WRYDA==
Client : TM4Jicrrvxjs829JD0wWsw==
Connection authenticated :
false
NOTE:
It is common for HMAC-based authentication to be used internally by software when it sets up communication with subprocesses. Just make sure you don't transmit the secret key along the unencrypted connection as anyone can easily sniff the traffic to pick it up.
By the way, authenticating a connection is not the same as encrypting a connection.
Happy coding!
References:
https://golang.org/pkg/crypto/hmac/
https://www.socketloop.com/tutorials/golang-simple-client-server-example
https://www.socketloop.com/tutorials/golang-how-to-generate-random-string
See also : Golang : Secure(TLS) connection between server and client
By Adam Ng
IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.
Advertisement
Tutorials
+8.8k nginx: [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
+15.3k Chrome : ERR_INSECURE_RESPONSE and allow Chrome browser to load insecure content
+51.6k Golang : How to get time in milliseconds?
+8.7k Golang : Build and compile multiple source files
+4.9k Python : Create Whois client or function example
+34.7k Golang : Upload and download file to/from AWS S3
+16.5k Golang : Set up source IP address before making HTTP request
+20.1k Nginx + FastCGI + Go Setup.
+5.5k Golang : Fix opencv.LoadHaarClassifierCascade The node does not represent a user object error
+13.2k Golang : Get user input until a command or receive a word to stop
+8.1k Golang: Prevent over writing file with md5 hash
+15.8k Golang : Loop each day of the current month example